Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
- ``article:section`` - based on the selected category/subcategory ([#858](https://github.com/flatpressblog/flatpress/pull/858))
- ``article:tag`` - based on the tags assigned using the tag plugin ([#858](https://github.com/flatpressblog/flatpress/pull/858))
- ``article:author`` - based on the blog author ([#858](https://github.com/flatpressblog/flatpress/pull/858))
- Dynamic Open Graph images ``og:image`` with a social media-friendly aspect ratio of 1.91:1 for posts and static pages. ([#939](https://github.com/flatpressblog/flatpress/pull/939))
- ``og:image:alt`` is derived from the optional image captions.
- BBCode plugin: update to version 2.0.4
- The HTML button is only displayed if inline HTML is allowed in BBCode. ([#867](https://github.com/flatpressblog/flatpress/pull/867))
- jQuery plugin update to version 2.2.2 ([#896](https://github.com/flatpressblog/flatpress/pull/896))<br><sub><i>Note: Edge Legacy, Internet Explorer 9–10, iOS 7+, and the Android browser on Android 4.0+ are no longer supported. If you need support for these legacy browsers, you must disable the jQuery plugin so that PhotoSwipe uses the older jQuery plugin 3.7.1 version.</i></sub>
Expand All @@ -50,6 +52,8 @@
- Re-registering the same email address replaces older, still-open pending tokens.
- Old confirmation links for the same address are thereby invalidated.
- If a local blocklist is missing, a download is attempted before the first form processing.
- ReadMore plugin update zu version 1.0.4
- Revised visibility layer ([#939](https://github.com/flatpressblog/flatpress/pull/939))

### Bugfixes
- PhotoSwipe plugin update to version 2.0.7
Expand Down
138 changes: 100 additions & 38 deletions fp-plugins/readmore/plugin.readmore.php
Original file line number Diff line number Diff line change
@@ -1,42 +1,118 @@
<?php
/**
* Plugin Name: ReadMore
* Version: 1.0.3
* Version: 1.0.4
* Plugin URI: https://www.flatpress.org
* Author: FlatPress
* Author URI: https://www.flatpress.org
* Description: Chops lengthy entries in the overview and appends a "read more" link. Part of the standard distribution.
*/

// $MODE specifies when you want to chop your entry
/**
* $MODE specifies when you want to chop your entry
*
* 'auto' will chop your entry at the value
* specified in $CHOP_AT
*
* 'manual' will chop your entry only when a [more] tag is found in
* the content
*
* 'semiauto' will chop your entry at the [more] tag. If no such a tag
* is found, the entry is chopped at the value specified in $CHOP_AT
*
* 'sentence' will chop your entry after $CHOP_AT sentences
*
* WARNING! 'auto' and 'semiauto' modes need improvements! unclosed tags
* at the chop point will probably result in validation errors!
* If you're willing to improve it (using a quick but efficient algorithm
* feel free and then let us know :) )
*
* we recommend using $MODE = 'manual' (SPB legacy behaviour :) )
*/

// 'auto' will chop your entry at the value
// specified in $CHOP_AT
/**
* Return a validated ReadMore mode.
*
* The optional argument is primarily useful to callers that need to inspect
* ReadMore semantics without redefining PLUGIN_READMORE_MODE.
*
* @param string|null $mode
* @return string
*/
function plugin_readmore_get_mode($mode = null) {
if ($mode === null) {
$mode = defined('PLUGIN_READMORE_MODE') ? (string) constant('PLUGIN_READMORE_MODE') : 'manual';
} else {
$mode = (string) $mode;
}

// 'manual' will chop your entry only when a [more] tag is found in
// the content
if (!in_array($mode, array('auto', 'manual', 'semiauto', 'sentence'), true)) {
return 'manual';
}

// 'semiauto' will chop your entry at the [more] tag. If no such a tag
// is found, the entry is chopped at the value specified in $CHOP_AT
return $mode;
}

// 'sentence' will chop your entry after $CHOP_AT sentences
/**
* Compute the part of an already-filtered entry that remains visible in a
* multi-entry stream.
*
* This contains the chopping algorithm used by plugin_readmore_main() but
* deliberately does not build links and does not inspect the current query.
* Other plugins can therefore reuse the exact ReadMore boundary without
* copying its mode-specific logic.
*
* @param string $string Content as ReadMore receives it (normally BBCode-rendered)
* @param string|null $mode Optional explicit mode
* @param int|null $chopAt Optional explicit threshold
* @return array{content:string,chopped:bool,suffix_prefix:string,mode:string}
*/
function plugin_readmore_get_stream_excerpt($string, $mode = null, $chopAt = null) {
$string = (string) $string;
$mode = plugin_readmore_get_mode($mode);
$chopAt = $chopAt === null ? 4 : max(1, (int) $chopAt);

$result = array(
'content' => $string,
'chopped' => false,
'suffix_prefix' => '',
'mode' => $mode
);

// Preserve the historical order: semiauto first behaves like auto and
// only reaches the manual [more] branch when the auto condition did not chop.
if ($mode === 'auto' || $mode === 'semiauto') {
if (strlen($string) > $chopAt) {
$result ['content'] = substr($string, 0, $chopAt);
$result ['chopped'] = true;
$result ['suffix_prefix'] = '&hellip; ';
return $result;
}
}

// WARNING! 'auto' and 'semiauto' modes need improvements! unclosed tags
// at the chop point will probably result in validation errors!
// If you're willing to improve it (using a quick but efficient algorithm
// feel free and then let us know :) )
if ($mode === 'manual' || $mode === 'semiauto') {
$p = strpos($string, '[more]');
if ($p !== false) {
$result ['content'] = substr($string, 0, $p);
$result ['chopped'] = true;
return $result;
}
} elseif ($mode === 'sentence') {
$matches = array();
$v = preg_match_all('|[.!?]\s|', $string, $matches, PREG_OFFSET_CAPTURE);
if ($v && count($matches [0]) > $chopAt) {
$result ['content'] = substr($string, 0, $matches [0] [$chopAt - 1] [1]);
$result ['chopped'] = true;
$result ['suffix_prefix'] = '. ';
}
}

return $result;
}

// we recommend using $MODE = 'manual' (SPB legacy behaviour :) )
function plugin_readmore_main($string) {
global $fp_params;

$MODE = defined('PLUGIN_READMORE_MODE') ? (string) constant('PLUGIN_READMORE_MODE') : 'manual';
if (!in_array($MODE, array('auto', 'manual', 'semiauto', 'sentence'), true)) {
$MODE = 'manual';
}

$CHOP_AT = 4; // characters or sentences

$lang = lang_load('plugin:readmore');
$readmoreString = $lang ['plugin'] ['readmore'] ['readmore'];

Expand All @@ -46,23 +122,9 @@ function plugin_readmore_main($string) {
if (($q && !$q->single) && !isset($_GET ['page'])) {
list ($id) = $q->getLastEntry();

if ($MODE == 'auto' || $MODE == 'semiauto') {
if (strlen($string) > $CHOP_AT) {
return substr($string, 0, $CHOP_AT) . "&hellip; <span class=\"readmore\"><a href=\"" . get_comments_link($id) . "#readmore-" . $id . "\">" . $readmoreString . "</a></span>";
}
}

if ($MODE == 'manual' || $MODE == 'semiauto') {
if (($p = strpos($string, '[more]')) !== false) {
return substr($string, 0, $p) . "<span class=\"readmore\"><a href=\"" . get_comments_link($id) . "#readmore-" . $id . "\">" . $readmoreString . "</a></span>";
}
} elseif ($MODE == 'sentence') {
$matches = array();
if ($v = preg_match_all('|[.!?]\s|', $string, $matches, PREG_OFFSET_CAPTURE)) {
if (count($matches [0]) > $CHOP_AT) {
$string = substr($string, 0, $matches [0] [$CHOP_AT - 1] [1]) . ". <span class=\"readmore\"><a href=\"" . get_comments_link($id) . "#readmore-" . $id . "\">" . $readmoreString . "</a></span>";
}
}
$excerpt = plugin_readmore_get_stream_excerpt($string);
if (!empty($excerpt ['chopped'])) {
return $excerpt ['content'] . $excerpt ['suffix_prefix'] . "<span class=\"readmore\"><a href=\"" . get_comments_link($id) . "#readmore-" . $id . "\">" . $readmoreString . "</a></span>";
}
}

Expand Down
121 changes: 121 additions & 0 deletions fp-plugins/seometataginfo/developer-docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# SEO Meta Tag Info — Developer Documentation

## Scope

Relevant plugin versions:

| Component | Version | Role in this documentation |
|---|---:|---|
| SEO Meta Tag Info | 2.3.2 | Primary subject |
| BBCode | 2.0.4 | Active BBCode grammar and `[img]` rendering |
| PhotoSwipe | 2.0.7 | `[gallery]`, `[photoswipegallery]`, `[photoswipeimage]`, and `[img]` override |
| Thumbnails | 1.1.1 | Preview/thumbnail generation behind `bbcode_img_scale` |
| ReadMore | 1.0.4 | Stream visibility boundary |
| Gallery captions | 1.0.3 | Author-managed per-gallery image titles persisted through FlatPress gallery helpers |
| FlatPress | 1.6.dev | Query, gallery, entry, hook, and filesystem APIs |

The current target range used by the project is **PHP 7.2 through PHP 8.5** and **Smarty 4.5.5 through Smarty 5.8.4**. The Open Graph image pipeline itself does not require Smarty-specific syntax.

This is developer documentation, not end-user documentation. The short end-user file `../doc_seometataginfo.txt` remains separate.

## Source-of-truth rule

When this documentation and the code disagree, **the current target code wins**. The most important implementation files are:

- `../plugin.seometataginfo.php`
- `../inc/og-content-image.php`
- `../inc/hw-helpers.php`
- `../inc/class.iniparser.php`
- `../inc/migrate_data.php`
- `../tpls/admin.plugin.seometataginfo.tpl`
- `../regression-test/*.php`

The image pipeline also depends on behavior defined by:

- `../../bbcode/plugin.bbcode.php`
- `../../photoswipe/plugin.photoswipe.php`
- `../../photoswipe/photoswipefunctions.class.php`
- `../../thumb/plugin.thumb.php`
- `../../readmore/plugin.readmore.php`
- `../../gallerycaptions/plugin.gallerycaptions.php`
- `../../gallerycaptions/admin_uploader_gallerycaptions.class.php`
- `../../../fp-includes/core/core.gallery.php`
- `../../../fp-includes/core/core.fpdb.class.php`

## Documentation map

1. [Architecture and lifecycle](architecture.md)
2. [Open Graph image pipeline](open-graph-image-pipeline.md)
3. [Metadata storage and administration](metadata-storage-admin.md)
4. [Plugin and core integrations](integrations.md)
5. [Configuration, caching, and HTTP behavior](configuration-and-caching.md)
6. [Security model](security.md)
7. [Compatibility](compatibility.md)
8. [Testing and static analysis](testing.md)
9. [API reference](api-reference.md)
10. [Maintenance and extension guide](maintenance.md)

## High-level responsibilities

SEO Meta Tag Info has several responsibilities that should remain conceptually separate:

- persist per-entry, static-page, category, tag, archive, and default SEO metadata;
- emit standard SEO meta tags and canonical URLs;
- emit Open Graph metadata;
- derive article metadata for single-entry views;
- select a content-aware Open Graph image;
- derive `og:image:alt` from the selected image's explicit BBCode `title` or Gallery caption, with site-title fallback;
- serve locally generated 1200 × 630 Open Graph image responses;
- expose per-entry SEO description/keywords to Smarty entry templates;
- provide an administration panel for the host-level `robots.txt`;
- migrate older SEO metadata layouts when explicitly enabled.

The content-aware image feature is deliberately implemented inside the SEO plugin. BBCode, PhotoSwipe, and Thumb are treated as rendering semantics and dependencies; they do not contain SEO-specific logic.

## Key invariants

Developers changing the image pipeline should preserve these invariants:

1. **Original media selection:** SEO selection is based on the original image source, never on a `.thumbs` preview.
2. **Source order:** the first valid visible media occurrence wins.
3. **Gallery order:** `gallery_read_images()` defines gallery ordering, while `gallery_read_captions()` supplies the title for the exact selected gallery file.
4. **ReadMore visibility:** a multi-entry stream must not publish media that ReadMore hides.
5. **Single/static completeness:** single entries and static pages may select media after `[more]`.
6. **No live media rendering during probing:** probing must not create thumbnails or advance PhotoSwipe state.
7. **Primary query safety:** scanning the stream must not consume or replace the page's active query state.
8. **Local-path containment:** local OG sources must resolve inside `IMAGES_DIR`.
9. **No remote fetch:** remote HTTP(S) images are validated syntactically but are not downloaded/proxied by the server.
10. **No distortion:** transformable local JPEG/PNG sources are fitted proportionally onto the configured OG canvas.
11. **Explicit invalid source does not become theme preview:** a requested but invalid `seometa_ogsource` remains an invalid content-image request.
12. **HTML-escaped query tolerance:** copied URLs containing literal `&amp;` / repeated `amp;` parameter prefixes are accepted without weakening parameter precedence or path validation.
13. **Image-description binding:** an explicit `[img ... title="..."]` or the Gallery caption of the exact selected file becomes `og:image:alt`; missing/empty titles fall back to `general.title` and never change image selection.
14. **No alt substitution:** a BBCode `alt` attribute, filename, IPTC title, thumbnail title, or caption from a later gallery image must not substitute for a missing user image title.

## Architectural overview

```mermaid
flowchart LR
A[FlatPress request] --> B[SEO Meta Tag Info]
B --> C{Dynamic OG image request?}
C -- yes --> D[Validate requested local source]
D --> E[Render or stream image response]
C -- no --> F[wp_head metadata routing]
F --> G[Content-aware image resolver]
G --> H[BBCode parser clone / media markers]
H --> I[ReadMore visibility, when stream]
I --> J[Original image or first valid gallery image]
J --> JA[Bind explicit image title / exact gallery caption]
JA --> K[OG metadata preparation]
K --> L[1200 x 630 endpoint for transformable local JPEG/PNG]
K --> M[Direct URL fallback for remote/unsupported media]
F --> N[Description / keywords / robots / canonical / article metadata]
```

## What is deliberately not done

- No DOM scraping of the final rendered page.
- No selection from `<img src>` because it may point to `.thumbs`.
- No server-side download of remote Open Graph images.
- No direct mutation of the BBCode, PhotoSwipe, or Thumb plugin for SEO purposes.
- No use of `get.php` as an image or thumbnail wrapper.
- No dependency on a specific Apache/nginx/IIS rewrite configuration for the OG endpoint.
Loading