Jump to content

artfulrobot

Members
  • Posts

    65
  • Joined

  • Last visited

  • Days Won

    1

Everything posted by artfulrobot

  1. I started with those, but they're not always the right way. There's also url segments and path() method overrides. The challenge is: 1. you want pages to be served at your desired url. 2. You want the admin ui to show correct URLs. 3. You want the link adding UI to find the correct URL. 4. You want $pages->get(url) to work. 5. You want the qa checker to work. 6. You want PagePath module to keep up with changes. If interested, here's a rambling post of my journey, I got there in end (except 2, but, meh!).
  2. Here's a copy of my blog with some reflections on building my first site with ProcessWire as someone coming from Drupal: peopleandplanet.org ProcessWire is an open source CMS Content Management System or Framework (CMS / CMF respectively) using PHP and MariaDB/MySQL. It's been around a while, humbly gathering users and sites. I spent a good while reviewing lots of open source CMSes recently as I have previously relied on Drupal 7 (excellent) and didn't like Drupal 8+ nor BackDrop (a fork of Drupal 7). WordPress? not with Gutenberg/React and all those plugin ads, thanks. Turns out I'm quite picky about CMSes! This is because my role is developer, trainer, implementer, themer, discusser of problems and solutions and dreams. I have personal relationships with my clients and am here to help. So I need a system that makes it simple for them to do as much as possible without me, while also being flexible enough for me to rapidly develop new features. So I was shopping for me and my clients, not just one of those parties. ProcessWire seemed a good balance, and once I started developing with it I was hooked. I've now launched my first site with it: peopleandplanet.org and my clients are pretty pleased with it, and I have another job in the pipeline. Templates and pages In ProcessWire, everything (even users!) is a Page, and every Page has a Template. So in Drupal-speak, it's kinda like Page = Content/Entity and Template = Content/Entity Type. A Template is configured with lots of options and describes what fields apply. Templates don't all need to be renderable, but typically are, so generally have an accompanying Template File. Key implementation decisions I made There are many ways to generate and theme page HTML in ProcessWire and I went through them all! Here's what I settled on: Use Page classes: these are PHP classes which add/bend functionality for a particular page/template. Doing pre-processing of data this way seemed the cleanest way to organise things, using inheritance to share code between similar templates. I used the excellent Latte templating engine instead of plain PHP or other options like Blade/Smarty/... Latte is context-aware which makes templates cleaner and clearer to look at and safer because it knows to escape content in one way as HTML text and another as an attribute, for example. The final clincher is that it uses the same syntax as PHP itself, so as a developer hopping between PHP and Latte, there's much less brain strain. Use PageTableNext. This is similar to Drupal's Paragraphs or Gutenberg's Blocks (ish). It allows a page to be built from slices/sections of various different templates, e.g. I have things like "text" and "text-over-image" and "animated stats" etc. These let authors make interesting pages, applying different preset styles to things, giving a good mix of creative control and theme adherence. What I liked Beyond the above features, I liked these things: Fairly unopinionated: the core is quite small and everything is a module, so when you write your own module, you have similar level of access. e.g. I was able to make a core module behave differently by using hooks rather than having to maintain a patch of a core code file. The selector API is a domain-specific query language for fetching page data that makes a lot of common things you need to do very easy and clear to read. I like readable code a lot. A lot of basic CMS features are implemented really nicely; thought has gone into them. e.g. Drupal has a redirect module that can add redirects from old URLs when you update the 'path alias' for a page - great - but ProcessWire's implementation (a) prevents you making circular redirects which is a quick way to kill a Drupal site by accident that's happened more than once, and (b) has some useful rules like let's only do this if the page has been in existence for a little while - because typically while first composing a page you may change the title quite a few times before you publish. e.g. when you save a page that has links to other pages in it, it stores the page IDs of those other pages too, and when the page is fetched it will check that the URLs exist and still match the ID, fixing them if needed. Images - have 'focus' built in as well as resizing etc. so if a crop is needed you can ensure the important content of the image is still present. Booting is flexible; it enabled me to boot ProcessWire from within Drupal7 and thereby write a migration script for a lot of content. There's a good community on the forum. A forum feels a bit old fashioned, but it's good because you can have long form discussions; it sort of doubles as a blog, and a lot of new features are announced in forum posts by Ryan and others. The Tracy debugger is mind-blowingly good, just wow. Modules - third party or core - are typically very focussed and often tiny. This is a testament to what can be achieved throught the flexible and well-designed APIs. Weekly updates on core development from both the lead developer on the blog and the community, both with RSS feeds so it's easy to keep updated. What I don't like Logging is weird and non-standard. I prefer one chronological log of well categorised events, but instead we have lots of separate files. This is a bit weird. One thing it is opinionated on is that there should be a strict hierarchy between pages and URLs. I like that level of order, but in real life, it's not what I needed. e.g. People & Planet has three main campaigns and it wanted those at /campaign-name not /campaigns/campaign-name. And we wanted news at /news/2024-03-25/title-of-news-article, but we don't want a page at /news/2024-03-25/ and we want the news index to be under an Info page in the menu. This meant I had to write a custom menu implementation to implement a different hierarchy to display what we wanted it to look (3 campaigns under a Campaigns menu, the news index under an Info menu). There was a page hook for having news articles describe their URLs with the publish date, but this took quite a bit of figuring out. Ryan Cramer leads/owns the project and other contributors are sparse. He seems like a lovely person, and I'm super grateful for his work, but there's only one of him, so this is a bit of a risk. Also, the code has no phpunit tests. Gulp. There have been various initiatives, but this sort of thing needs to come from a core team, and it's not a priority for Ryan. I love tests, they help avoid regressions, help document designed behaviour, etc. Likewise, there's a styleguide, but it's not adhered to, so... Right decision? I'm very happy with it, and it seems a great fit for my clients' needs, and they're very happy. All software has risks and I got burned twice with Drupal 8/9 - once backing out after months of development; another project that went to completion I regret and dislike administering/maintaining now. But so far, so good, and I intend to use ProcessWire for other projects (including replacing this website) very soon. Contributions I have two ProcessWire modules that are there for anyone who needs them. TagHerder provides a page at Setup » Tag Herder that lists every tag and every page tagged with that tag, and provides three options: Rename the tag; Trash the tag; Replace the tag with another. Useful for cleaning up tags that have gotten out of control! EditablePublishedDate lets you edit the published date of a page. Useful for entering historical page information or such.
  3. I think my problem may have been caused by the code in https://processwire.com/talk/topic/29716-mysterious-blank-page-no-errors-to-be-found-andor-how-to-use-alternate-urls-path-pages_paths-table/?do=findComment&comment=239935 because $page->published is not how to tell if a page is published(!) and it has a value for unpublished pages, too. Instead I should have been using $page->isUnpublished() I also needed this to ensure that the PagePaths module kept its table up-to-date: $pagePathModule = modules('PagePaths'); $this->pages->addHook('published', $pagePathModule, 'hookPageMoved'); $this->pages->addHook('unpublished', $pagePathModule, 'hookPageMoved');
  4. I have no idea if this is of interest to anyone else, but it might be useful for me to document my discovery journey, so I'll continue spouting into the void! Having looked into how PW fetches pages specified by path, what have I learnt? $pages->pathFinder() is described as a newer more capable way to find pages by path, but it's not the method that is used by $pages->get() / the PageLoader class. (That PageLoader::getByPath() method does/can use pathFinder but only in the case that its searches bring up multiple possibilities. The QA/link checker code introduces a concept of sleep/wake which as far as I can guess, it calls sleep when the HTML is written to the database which tries to add an attribute in to identify the page a link goes to by ID. And wake when loading the HTML, possibly replacing URLs with something based on the previously stored ID. I'm not 100%. The QA code calls $pages->getByPath() up to twice. The first time it looks, given /a/b/c for a page whose name is c and whose parent page's name is b and whose parent page is named a; if that fails it checks whether the page is covered by a template using urlSegments; if that fails it checks the history table (if enabled). Then if not found, it calls again without urlSegments, which causes it to also search for a page called c - if there's only one, then it's happy. I'm still puzzling over why the QA method seems to be different from the normal method. Perhaps it's because the QA can't know about access restrictions of the user who will access the link, so only goes on existence? why a page class' path() output seems to be ignored - I realise that the only way not to ignore it would be to rely on the optional PagePaths module, but that seems reasonable, and is hard wired into core anyway in other places (e.g. multiple matches). If getByPath() used that, perhaps via pathFinder would that fix things I wonder?
  5. So the link validator really strictly imposes the hierarchy. Given a path /a/b/c it looks for a page named 'c' whose parent is named 'b' whose parent is named 'a'. Which is why it fails. I can quieten the false link validation errors if I enable url segments on the /news/ template - but then the link validation thinks /news/anything exists because it has a handler - even though the handler might give a 404. I would like to see a different resolve path to page mechanism in use that could accommodate these other layouts. I'm unclear how and when the PagesPaths module is brought in to assist in this process too. I understand it's supposed to sort of be a bit of a cache to assist lookups. I'd think something like: Does the path map to a page in pages_paths? Great, super quick indexed lookup, use that. If not... Hookable path-to-page method. Tries first the original way (above), if not... if page handled via segments, has some way to ask the controller for the page to use (may be itself, or some other), if not check path history (what about URL hooks?) invalid.
  6. Update: so this doesn't solve it. Why? Because the automatic link selector will offer you the facade links to select, but then when you save it complains that the link is wrong and, although it has saved, it won't let you off the screen because it considers it a validation error. :shrug: I think I'm going to need to grok the pages() get code to see if there's a way around this. The whole public-facing URLs must match an internal hierarchy constraint is a real bind.
  7. Here's how I solved bodged it: public function path() { // We're including a trailing slash, even though it's configured not to use them. if (!$this->published) { // Before a page is published, return the basic, hierarchical path. // This will get stored in pages_paths table, which is important so the // page can be found by its hierarchical path. return '/news/' . $this->name . '/'; } // On a published page, return our facade. return '/news/' . date('Y-m-d', $this->published) . '/' . $this->name . '/'; } When calling the path() with an unpublished page, return a plain path that matches the actual hierarchy. This results in the correct value being written to the pages_paths table. Then on a published page, return the facade.
  8. Symptoms: as title says, just a blank page, no content, but successful http response. Nothing logged in any of PW's log files, or in php's error log. Cleared caches, still same problem. Other pages using same template all work fine. Page loadable with pages(1234) but *not* loadable with pages('/path/name-of-page') - the latter gets null page object. Workaround: I changed the page's name, saved, then changed it back again, and it seems to work. Possible(?) confounding factors These news pages live under /news/ however I wanted the URLs to include the published date, like /news/2024-02-21/some-page It is being accessed via URL hook on /news/[0-9-]{10}/(title[^/]+) with $event->pages->get("/news/$event->title"); The page class overrides the `path()` function which contains `return '/news/' . date('Y-m-d', $this->published) . '/' . $this->name . '/';` This has worked fine for all the other pages. Any ideas what might have happened? What could cause pages() to fail to load by page path for this page? Possible asides: I'm not 100% on how to use different urls for pages, but what I have seems/ed to work; for example the template is configured not to use trailing slash, but I found I needed one returned by path(). pages('news/the-page') (without the date) loads the page. So I think path() is only used for rendering. It didn't look like the date-prefix is persisted in the db. EDIT: page_path table When it was broken, I did a mysqldump. And after it was fixed I repeated. I then used neovim to diff the dumps (mmm, fun). Here's what I found. I believe the key difference is in the pages_paths table, where the broken version had 'news/1970-01-01/the-page' and the working version had 'news/2024-02-21/the-page'. My hunch is that something populated the pages_paths table before the page was published, thereby getting the epoch date. So I need to understand the relationship between a class's path() function, and loading with pages($path).
  9. Hi @kaz I began trying to answer you, but it's so hard to talk about these things in text! Not sure if this picture I made to document my setup is any use?
  10. At risk of making myself unpopular, I'm a neovim user who has ethical aversion to AI. I get a great developer experience and I don't have to worry about Kernighan's law making my code buggy and hard to maintain. I am also concerned about climate change, and AI has huge carbon emissions - (re)training an LLM has been estimated to emit the equivalent CO2e as 125 round-trip flights between New York and Beijing [source]. I'm concerned that my open source code has been used against its license to train AI through github (find my stuff on codeberg or project owed gitlabs) and so now will be included in proprietary products. I'm also against having to pay subscriptions to a handful of big tech companies because this accelerates wealth inequality and erodes democracy, reducing our chances of turning the ship around before crossing the runaway climate change event horizon. I do not doubt AI's usefulness, or it's ability to charm and amaze or excite, I do not doubt that it can be used to save lives even (e.g. specific medical use such as identifying cancer early from scans). But I do remember how Nobel thought his invention of dynamite would be great and I'm not going to be taken in by another big tech lie about "don't be evil". We have very weak regulatory powers and a world teetering on the edge of unfathomable suffering. This is a post about a personal choice of text editor, and I'm not judging folx who choose Cursor (or ai plugins for neovim!) - I respect that opinions differ. But I wanted to share my opinions on my choice, too.
  11. I've just made a module that exposes the 'published' date under the page's Settings tab, so you can update it. It appears to be working nicely, but any feedback would be very welcome as I'm new to this still. https://codeberg.org/artfulrobot/EditablePublishedDate/src/branch/main/EditablePublishedDate.module.php
  12. I only have one pw site at the mo, and I can't reproduce it reliably. It's just happened a few times and I wondered if anyone else had the problem. The main bother of it is that, typically I'm in a hurry to debug the crash, not debug the debugger at the time of the crash!
  13. @adrian oh yeah, I'm all about the hard reload. I have basically stuck tape over my Ctrl key. But sadly it didn''t help. It's so weird how it exhibits the error, yet quotes the corrected code!
  14. I've noticed this a couple of times. I load a page and it crashes, Tracy gives me a big red page with helpful info on it. I then fix the problem, and reload the page - but the error won't go away. For example the screenshot below shows and error saying "Call to undefined function ProcessWore\pappub_inlay_request()", and then highlights a line in my code that (now because it's been fixed) doesn't show the code from the error message! One part of the page is lying to me, and it doesn't go away until some timeout. I'm guessing a cache timeout? But I've tried adding a ?something=123 var and that didn't fix anything either: still received the old error that no longer correlates to the actual code; Tracy seems to be intercepting my actual code to show me the old error! Anyone else had this?
  15. Thanks for sharing this @BitPoet I read about it in the newsletter. I love that people publish simple useful solutions like this because it is a great learning aid as someone who still considers themself new to ProcessWire. On looking at your code, I had these observations (which I'm only sharing in case they are interesting!) Wow, how cool that PW lets you invent new 'subfields' as a way to integrate potentially complex SQL. I wonder what I might do with that! Hmmm. Why escape what must only be an integer as a string? I then wondered how MariaDB would handle the comparison since you're requiring an implicit coercion by comparing an int (output of CHAR_LENGTH) to a string. Because '2' > '100' if you're comparing string values, which is not what we want. It seems from my tests that MariaDB picks integer as the shared type for the comparison, which is lucky. I assume the operator does not need validating because we're assuming PW core has already done this and there's no codepath that would allow this function to be called with user input for the operator. SELECT '2' > '100', CHAR_LENGTH('12') > '100', '100' < CHAR_LENGTH('12'); +-------------+---------------------------+---------------------------+ | '2' > '100' | CHAR_LENGTH('12') > '100' | '100' < CHAR_LENGTH('12') | +-------------+---------------------------+---------------------------+ | 1 | 0 | 0 | +-------------+---------------------------+---------------------------+ Anyway, thanks for sharing the code, I'm not sure if I'll need a length search any time soon, but it's helpful to have learnt that it's possible and that similar such extensions are possible.
  16. V interesting article! Thanks all. I have also implemented latte as the main templating engine in a site. I didn't use any of the big module solutions because I think my needs are simple and fewer dependencies leads to easier long term maintenance. I have implemented latte as an api var and used page classes for all the page type specific work. It's been good as I've leveraged inheritance in both page classes (I'm also using pageTableNext) and also latte (`{layout...}`), reducing duplicate code. One observation I had was that I like to use latte to make html templates more **readable**, and do all the heavy lifting **logic** in pure php in page classes. (I've also observed that I'm that committed to markdown that I use it instead of fiddly, non-semantic buttons I'd have to scroll back to get to! Oops)
  17. Came here because I use page table next and my staff users cannot delete a section they've added. They also can't delete any other pages, despite the permissions being allowed. * p1 Home * p2 Some page (ptn field contains p5, p6...) * p3 Admin * p4 Content elements * p5 a ptn section belonging to p2 * p6 another ptn page belonging to p2 Someone with staff role has permission to delete pages, but the delete button next to p5, p6 on the edit screen for p2 does nothing - the response comes back silently with nothing happening (JSON returned includes `success: false, message:""`) But this problem for me was solved by:
  18. @d'Hinnisdaël done, sorry for barking up the wrong tree!
  19. By the way, both the Getting Started link (to an #anchor) and the Documentation link (404: https://raw.githubusercontent.com/wanze/TemplateEngineFactory/v1.1.3/DOCUMENTATION.md) are broken at https://processwire.com/modules/template-engine-factory/
  20. Hi @d'Hinnisdaël This looks interesting, thanks for posting. By the way, both the Getting Started link (to an #anchor) and the Documentation link (404: https://raw.githubusercontent.com/wanze/TemplateEngineFactory/v1.1.3/DOCUMENTATION.md) are broken at https://processwire.com/modules/template-engine-factory/
  21. @BitPoet Are you doing this locally or are you preparing a PR for the project? If so you're brave! I bet someone out there is relying on the existing behaviour. e.g. for cache warming or some other purpose for which my imagination was too limited. There's specific code written to implement the existing behaviour, so that ought to be removed if you're going to throw an exception. Clearly someone, @ryan maybe, had something in mind, although I stand by my opinion that it's super confusing 😆
  22. Thanks both. I think the confusion arises because age is used to mean 2 different things in the cache context. In code, it's $expires in both contexts. For save() and also for get() when a function is also provided $expires means when the data should expire. For get() when a function is NOT provided $expires means "get if the thing expires before this new $expires, oh but also if it never expires (because of that 2010 date that's used to mean never expire...)". The 2nd case is my beef! Typically, when wanting a cached value, you care to fetch it unless it's too old for you to rely on. I can see the use case that you cache something with a long expiry, but that one user might only want to use a value if it's not that old. But that's not what we have. We have sort of the opposite: get something if it's going to expire before the given date. I'd love it if someone could give a scenario in which this was useful. Perhaps the use-case is cache warming? Like you might want to get stuff that's going to expire soon, and update the cache now. We could then use get($name, $expiry) and then recalculate the value and save() with a new expiry? This is the only thing I can think of? The fact that $expiry means 2 completely different things in the same get() api call is a trip hazard that needs flagging in the docs. I spent ages wondering why caching wasn't working and eventually stepped through to look at the SQL to get here.
  23. According to the docs at https://processwire.com/api/ref/wire-cache/get/ for $cache->get($key, $expiry), $expiry means: I thought that meant: at 9am store something with an hour's expiry (so 10am) using $cache->save($k, $v, 60*60); At 9:01am, ask for it, if the value is younger than 30 mins, using $cache->get($k, 30*60); But it doesn't. And it seems to me it can't do as it's documented to do because the date the thing was stored (and therefore how old it is) is not stored in the db table. In fact the SQL that's run is: SELECT * FROM caches WHERE name=$k AND expires <= 9:31am So even though our data is 1 minute old, it's not considered younger than 30 mins old and is not returned. The only way the cache could honour the docs would be if the date the thing was stored in the db was saved. Then we could do whenSaved < 9:31am (though I'd suggest we also specify that it hasn't expired too, to honour the save() contract). But we don't have that field. Just wanted to sanity check these findings with the community. And if I'm right, how do we go about suggesting clarifications for the docs? If I'm just a confused noob, please explain the designed use-case for the documented get/expiry behaviour.
  24. If it's ok to dig up old threads.... @bernhard here, aren't you double encoding url, title? Assuming this is front end code, with `$pages->of(true)` and you have html entities text formatter in place on your title, url fields then a title with - say - an & would come out looking like &amp; as it would actually be &amp;amp;
×
×
  • Create New...