Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC Micro/Lazy Stenope #111

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build_static.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: 'Build static'

on:
workflow_dispatch: ~
push:
branches:
- master
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,6 @@ Stenope is not a ready-to-use bloging system: but you could quickly _write your
<!-- TODO - [Adding custom files to the build]() -->
<!-- TODO - [Data source: writing a custom Provider]() -->
<!-- TODO - [Data format: writing a custom Decoder]() -->
- [Data manipulation: writing a custom Processor](doc/processors.md)
- [Lazy / Micro Stenope](doc/cookbooks/lazy-stenope.md)
- [Data manipulation: writing a custom Processor](doc/cookbooks/processors.md)
<!-- TODO - [How to automatically deploy and host a static site]() -->
14 changes: 13 additions & 1 deletion config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Stenope\Bundle\Command\DebugCommand;
use Stenope\Bundle\ContentManager;
use Stenope\Bundle\ContentManagerInterface;
use Stenope\Bundle\Controller\GenericContentController;
use Stenope\Bundle\Decoder\HtmlDecoder;
use Stenope\Bundle\Decoder\MarkdownDecoder;
use Stenope\Bundle\DependencyInjection\tags;
Expand All @@ -29,6 +30,7 @@
use Stenope\Bundle\Processor\AssetsProcessor;
use Stenope\Bundle\Processor\CodeHighlightProcessor;
use Stenope\Bundle\Processor\ExtractTitleFromHtmlContentProcessor;
use Stenope\Bundle\Processor\GenericContentTypesProcessor;
use Stenope\Bundle\Processor\HtmlAnchorProcessor;
use Stenope\Bundle\Processor\HtmlExternalLinksProcessor;
use Stenope\Bundle\Processor\HtmlIdProcessor;
Expand All @@ -42,6 +44,7 @@
use Stenope\Bundle\Routing\RouteInfoCollection;
use Stenope\Bundle\Routing\UrlGenerator;
use Stenope\Bundle\Serializer\Normalizer\SkippingInstantiatedObjectDenormalizer;
use Stenope\Bundle\Serializer\Normalizer\StdClassDenormalizer;
use Stenope\Bundle\Service\AssetUtils;
use Stenope\Bundle\Service\Git\LastModifiedFetcher;
use Stenope\Bundle\Service\NaiveHtmlCrawlerManager;
Expand All @@ -53,6 +56,7 @@
use Symfony\Component\Asset\Packages;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Mime\MimeTypesInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\String\Slugger\SluggerInterface;
Expand Down Expand Up @@ -142,6 +146,9 @@

// Serializer
->set(SkippingInstantiatedObjectDenormalizer::class)->tag('serializer.normalizer')
->set(StdClassDenormalizer::class)->args([
service(PropertyAccessorInterface::class),
])->tag('serializer.normalizer')

// Decoders
->set(MarkdownDecoder::class)
Expand Down Expand Up @@ -188,7 +195,11 @@
// HTML Crawler Manager
->set(NaiveHtmlCrawlerManager::class)
->set(SharedHtmlCrawlerManager::class)
->alias(HtmlCrawlerManagerInterface::class, NaiveHtmlCrawlerManager::class);
->alias(HtmlCrawlerManagerInterface::class, NaiveHtmlCrawlerManager::class)

// Controller
->set(GenericContentController::class)->public()->autowire()->autoconfigure()
;

// Tagged processors:
$container->services()->defaults()->tag(tags\content_processor)
Expand All @@ -199,6 +210,7 @@
'$logger' => service(LoggerInterface::class)->nullOnInvalid(),
]),
])
->set(GenericContentTypesProcessor::class)
->set(SlugProcessor::class)
->set(HtmlIdProcessor::class)
->args([
Expand Down
4 changes: 3 additions & 1 deletion doc/app/src/Controller/DocController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ public function index()
}

/**
* @Route("/{page}", name="page")
* @Route("/{page}", name="page", requirements={
* "page": ".+[^/]$",
* })
*/
public function page(Page $page)
{
Expand Down
232 changes: 232 additions & 0 deletions doc/cookbooks/lazy-stenope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# Lazy Stenope

Lazy or Micro Stenope is a lightweight opinionated alternative on how to create
a Symfony application for generating a static site, with minimal knowledge, code
and requirements.

Most notably, for basic usages, it's only required to know about Twig
(templating), but **you don't need to write custom code to serve your content**.
Start right away by writing the content in the format you like, write the
templates to render them and map the pages together, so it'll be dumped and
accessible from the static generated version.

It makes Stenope an alternative comparable to some other generation tools like
Hugo, **where you only need to write content and templates**, while still being
able to feel at home and tweak your app with your Symfony's knowledge if needed.

## How to start

Right after creating a Symfony app and requiring Stenope using Composer,
configure the Lazy Stenope stack in your `src/Kernel.php`:

```diff
<?php
namespace App;

+use Stenope\Bundle\MicroStenopeKernelTrait;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
// [...]

class Kernel extends BaseKernel
{
use MicroKernelTrait;
+ use MicroStenopeKernelTrait;

protected function configureContainer(ContainerConfigurator $container): void
{
// [...]
+ $this->configureStenope($container);
}

protected function configureRoutes(RoutingConfigurator $routes): void
{
// [...]
+ $this->configureStenopeRoutes($routes);
}
}

```

It'll register two routes for showing and listing contents:

| Route | Path (default) | Description |
| - | - | - |
| `stenope_show` | `/{slug}` | Show a specific content |
| `stenope_list` | `/{type}/list` | List all contents of a given type and sub-types |

as well as preconfiguring Stenope to load contents from a `content` directory on
your filesystem.
The equivalent config is:

```yaml
stenope:
providers:
Stenope\Bundle\Content\GenericContent: '%kernel.project_dir%/content'
resolve_links:
Stenope\Bundle\Content\GenericContent: { route: stenope_show, slug: slug }
```

Since your application would need an entrypoint, you can create a basic home
route and template using:

```yaml
# config/routes.yaml
home:
path: /
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
defaults:
template: home.html.twig
```

and the following templates structures:

```treeview
templates/
├── base.html.twig # base layout for your site
└── home.html.twig
```

## Writing contents

You can write content in [any supported format](../supported-formats.md), with
any data inside ; on the contrary of the "full-stack" Stenope approach, there is
no truly enforced structured model to which will be mapped your content.
Instead, a `GenericContent` is used and accepts any property.
Any structured data you write in your content is mapped to a property defined at
runtime on this content object.

E.g, such content:

```md
---
title: "Sunt amicitiaes desiderium ferox, placidus liberies."
date: 2021-09-10
draft: true
---

Always cosmically develop the wonderful sinner.
```

will instantiate a `GenericContent` object with `title`, `date` and `draft`
properties from the markdown header metadata. Specific to
the [markdown format](../supported-formats.md#markdown), the `content` property
is created as well, using the main file content.

Additionally, it always defines these 3 properties:


| property | description |
| - | - |
| `slug` | the content identifier, i.e: its file path. |
| `type` | the content main type, i.e: its folder (e.g: a `users/john.md` content is of type `users`) |
| `types` | the content types, i.e: its folder hierarchy (e.g: a `users/legacy/sarah.md` content has types `users/legacy` and `users`) |

## Writing templates

Lazy-Stenope discovers the template files to use for your contents by
convention.

### Show templates

At first, it'll attempt to find a template matching the exact directory
structure of your content, so a `users/john.md` content could be rendered using
a `templates/users/john.html.twig` template.

If this template is not found, it'll attempt to look at the content type(s) and
search for a `show.html.twig` template. This way, you can use a same template to
render similar types of contents, based on the directory structure and
organization of your contents.

Eventually, if no matching template is found so far, it'll make a last attempt
to load a generic `templates/stenope/show.html.twig` template.

You can also explicit the template to use for a content using the `template`
property:

```md
---
template: 'other/an-explicit-template.html.twig'
title: "Sunt amicitiaes desiderium ferox, placidus liberies."
---

Always cosmically develop the wonderful sinner.
```

To sum up, it'll attempt to load a template in the following order:

1. Explicit template path from the `template` property
1. `templates/{slug}.html.twig`
1. `templates/{type}/show.html.twig`
1. `templates/{parentType}/show.html.twig`
1. `templates/stenope/show.html.twig`

#### Available variables

The following variables are exposed to the template:

| variable | description |
| - | - |
| `content` | the `GenericContent` object instantiated from your content file |

#### Generate a link

You can generate a link to a content using its slug and:

```twig
<a href="{{ path('stenope_show', { slug: 'users/john' }) }}">John's profile</a>
```

### List templates

Contents inside a directory structure have one or more types registered, which
you can use to categorize and differentiate some common types of contents (e.g:
a `users/legacy/sarah.md` content has types `users/legacy` and `users`).

Typed contents can be listed on a page by rendering
a `templates/{type}/list.html.twig`
template, so contents inside a `users` directory could be rendered using
a `templates/users/list.html.twig` template.

Eventually, if no matching template is found so far, it'll make a last attempt
to load a generic `templates/stenope/show.html.twig` template.

To sum up, it'll attempt to load a template in the following order:

1. `templates/{type}/list.html.twig`
1. `templates/stenope/show.html.twig`

#### Available variables

The following variables are exposed to the template:

| variable | description |
| - | - |
| `type` | current type in the URL |
| `types` | the types hierarchy for the type in the URL |
| `contents` | the objects instantiated from your content files, filtered by the type in the URL |

#### Generate a link

You can generate a link to a content using its slug and:

```twig
<a href="{{ path('stenope_list', { type: 'users' }) }}">List users</a>
```

### Template directory structure reference

```treeview
templates/
├── base.html.twig
├── projects.html.twig ➜ renders the `projects.{yaml,json,md,…}` content
├── home.html.twig
├── stenope/
│ ├── list.html.twig ➜ renders a listing for a type with no matching list template
│ └── show.html.twig ➜ render a content without matching template
└── users/
├── legacy/
│ ├── list.html.twig ➜ renders a listing for a content from the `users/legacy` directory
│ └── show.html.twig ➜ renders a content from the `users/legacy` directory
├── list.html.twig ➜ renders a listing for a content from the `users` directory
└── show.html.twig ➜ renders a content from the `users` directory
```
File renamed without changes.
32 changes: 32 additions & 0 deletions src/Content/GenericContent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/*
* This file is part of the "StenopePHP/Stenope" bundle.
*
* @author Thomas Jarrand <[email protected]>
*/

namespace Stenope\Bundle\Content;

class GenericContent extends \stdClass
{
public string $slug;
public ?string $template = null;
public ?string $type = null;
/** @var string[] */
public array $types = [];

public static function expandTypes(string $type): array
{
$types = [$base = $type];

while (true) {
if ('.' === $base = \dirname($base)) {
break;
}
$types[] = $base;
}

return $types;
}
}
3 changes: 3 additions & 0 deletions src/ContentManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

class ContentManager implements ContentManagerInterface
{
public const STENOPE_CONTEXT = 'stenope_context';

private DecoderInterface $decoder;
private DenormalizerInterface $denormalizer;
private PropertyAccessorInterface $propertyAccessor;
Expand Down Expand Up @@ -229,6 +231,7 @@ private function load(Content $content)
$this->crawlers->saveAll($content, $data);

$data = $this->denormalizer->denormalize($data, $content->getType(), $content->getFormat(), [
self::STENOPE_CONTEXT => true,
SkippingInstantiatedObjectDenormalizer::SKIP => true,
]);

Expand Down
Loading