How-to guides
Each guide starts from a situation you may run into, shows the code, and lists what to watch out for. Put the PHP in a small plugin of your own (or in a child theme’s functions.php) and rename the my_ prefixes to your own. The code is tested against RowSprout 3.1; the hooks it uses are described in the hook reference.
All guides work with the free plugin except the last two, which need RowSprout Pro.
Use a row’s values in your theme
Section titled “Use a row’s values in your theme”Situation. Your theme, or another plugin, needs a value from the row a page was generated from, for example the town of the current page for a “Serving Amsterdam and surroundings” line, a schema.org block or a contact widget.
RowSprout does store each value on the generated page, but under keys that contain internal field IDs (_rowsprout_page_field_id_1494594038). Those differ per template and are not meant to be coded against. Instead, write each value under a key of your own, made from the property’s code, whenever a page is generated:
/** * Stores every property of a generated page under a readable meta key: * the property with the code "town" becomes "rs_town". */add_action( 'rowsprout_page_after_upsert', function ( int $pageId, array $group ): void { foreach ( $group['fields'] ?? [] as $key => $field ) { $code = sanitize_key( (string) ( $field['code'] ?? $key ) ); $value = (string) ( $field['value'] ?? '' ); if ( $code === '' ) { continue; } if ( $value === '' ) { delete_post_meta( $pageId, 'rs_' . $code ); } else { update_post_meta( $pageId, 'rs_' . $code, $value ); } }}, 10, 2 );In a template file of your theme:
if ( is_singular( 'rowsprout_page' ) ) { $town = get_post_meta( get_the_ID(), 'rs_town', true ); if ( $town !== '' ) { echo '<p class="service-area">Serving ' . esc_html( $town ) . ' and surroundings</p>'; }}Watch out for
- Existing pages get the new keys the next time they are generated: save their template once with Create & update pages.
- For a child template,
$groupalready has the values it inherits from its parent filled in. - The value is the stored value, so escape it for wherever you print it.
List the pages of a template
Section titled “List the pages of a template”Situation. You want your own list of generated pages: “Other towns we work in” at the bottom of every page, a footer menu, or an export. For a plain list or cards, the Grid block, Elementor widget or WPBakery element already does this without code.
Every generated page records the template it came from in the post meta _rowsprout_page_source_template_id:
/** * The published pages generated from one template, sorted by title. * * @return WP_Post[] */function my_rowsprout_pages( int $templateId, array $args = [] ): array { return get_posts( $args + [ 'post_type' => 'rowsprout_page', 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC', 'meta_key' => '_rowsprout_page_source_template_id', 'meta_value' => $templateId, ] );}“Other towns” on a generated page, linking to the template’s other pages:
if ( is_singular( 'rowsprout_page' ) ) { $templateId = (int) get_post_meta( get_the_ID(), '_rowsprout_page_source_template_id', true ); $others = my_rowsprout_pages( $templateId, [ 'post__not_in' => [ get_the_ID() ] ] );
if ( $others ) { echo '<ul class="other-towns">'; foreach ( $others as $page ) { printf( '<li><a href="%s">%s</a></li>', esc_url( get_permalink( $page ) ), esc_html( get_the_title( $page ) ) ); } echo '</ul>'; }}Watch out for
- With WPML, every language of a template is a template of its own, so this lists the pages of the current language only.
_rowsprout_page_source_group_idholds the row’s ID, which is only unique within its template. Always combine it with the template ID.
Add your own property type
Section titled “Add your own property type”Situation. Your rows need a value the built-in types do not check or format, for example the coordinates of each location, which should become a map link.
A property type is a class implementing FieldTypeContract; extending BaseFieldType gives you the defaults. Register it with the rowsprout_field_types filter, and create the object inside the filter: your plugin may load before RowSprout, and the base class only exists once RowSprout is loaded.
use RowSprout\Core\Template\FieldTypes\BaseFieldType;
add_filter( 'rowsprout_field_types', function ( array $types ): array { $types[] = new class() extends BaseFieldType { public function getType(): string { return 'coordinates'; }
public function getUiKind(): string { return 'text'; // the input shown in the groups table }
protected function label(): string { return __( 'Coordinates', 'my-plugin' ); }
protected function description(): string { return __( 'Latitude and longitude, for example 52.3676, 4.9041.', 'my-plugin' ); }
protected function defaultCode(): string { return 'coordinates'; }
/** Keeps "latitude,longitude"; anything else is stored as empty. */ public function sanitize( string $value, array $options ): string { if ( ! preg_match( '/^\s*(-?\d{1,2}(?:\.\d+)?)\s*,\s*(-?\d{1,3}(?:\.\d+)?)\s*$/', $value, $m ) ) { return ''; } $lat = (float) $m[1]; $lng = (float) $m[2]; if ( abs( $lat ) > 90 || abs( $lng ) > 180 ) { return ''; } return round( $lat, 6 ) . ',' . round( $lng, 6 ); } }; return $types;} );The type now appears in the Type list on the Properties tab, and its placeholder token inserts the coordinates as text. To turn it into a map link when a Button or Image block’s link is bound to it (Block Bindings), handle the type in rowsprout_property_binding_url:
add_filter( 'rowsprout_property_binding_url', function ( $url, string $value, string $type ) { if ( $type !== 'coordinates' || $value === '' ) { return $url; } [ $lat, $lng ] = explode( ',', $value ); return sprintf( 'https://www.openstreetmap.org/?mlat=%1$s&mlon=%2$s#map=15/%1$s/%2$s', $lat, $lng );}, 10, 3 );Watch out for
getType()is stored in every template that uses the type. Never change it once the type is in use.sanitize()runs when a row is saved, so a value that does not pass is stored as empty, not rejected with a message.- The hook reference lists the other filters for property types, such as
rowsprout_property_binding_htmlfor text output.
Keep another plugin’s data off generated pages
Section titled “Keep another plugin’s data off generated pages”Situation. When a page is generated, RowSprout copies all post meta of the template onto it, with the placeholder tokens replaced. That is what makes SEO titles and page builder layouts work per page. But some plugins store data that belongs to one post only, such as a view counter, or a cache built from the content that each page has to rebuild for itself.
// Never copied: each generated page keeps its own value.add_filter( 'rowsprout_excluded_meta_keys', function ( array $keys ): array { $keys[] = 'post_views_count'; // a view counter $keys[] = '_my_stats_*'; // a trailing * matches every key with this prefix return $keys;} );
// Copied, but deleted again right after every rebuild, so the page builds its own.add_filter( 'rowsprout_page_delete_post_meta_keys', function ( array $keys ): array { $keys[] = '_my_toc_cache'; return $keys;} );Watch out for
- Excluded keys are only skipped. A value already copied onto a page by an earlier generation stays there until you delete it.
- Rank Math’s and Elementor’s own keys are already handled by RowSprout.
Tell another system when a page changes
Section titled “Tell another system when a page changes”Situation. Another system has to know when a generated page is created or updated: a search index, a CRM, a “new location” message in a chat channel.
rowsprout_page_after_upsert fires after every generation of a page, in the background queue. Hand the work off without waiting for the answer, so a large batch is not slowed down:
add_action( 'rowsprout_page_after_upsert', function ( int $pageId, array $group, int $templateId ): void { if ( get_post_status( $pageId ) !== 'publish' ) { return; }
wp_remote_post( 'https://hooks.example.com/rowsprout', [ 'blocking' => false, // don't wait for the reply 'headers' => [ 'Content-Type' => 'application/json' ], 'body' => wp_json_encode( [ 'url' => get_permalink( $pageId ), 'title' => get_the_title( $pageId ), 'template' => $templateId, 'row' => $group['id'] ?? null, ] ), ] );}, 10, 3 );Watch out for
- A page is generated again on every Create & update pages of its template, even if nothing changed for that page. Without RowSprout Pro’s Smart Generate, one save of a template with 300 rows means 300 calls. Compare a hash of the content if the receiver should only hear about real changes.
- Page caches (WP Rocket and others) already clear a page when WordPress updates it; you do not need this hook for that.
Generate pages more slowly
Section titled “Generate pages more slowly”Situation. On a small server, generating hundreds of pages at once slows the site down, or you only want pages to be built at night.
The background queue runs every minute and starts up to 40 pages per run. Both the batch size and the moment are filters:
// At most 10 pages per run.add_filter( 'rowsprout_groups_queue_limit', function (): int { return 10;}, 20 );
// Only generate between 01:00 and 06:00, site time.add_filter( 'rowsprout_is_within_processing_window', function ( bool $within ): bool { $hour = (int) wp_date( 'G' ); return $within && $hour >= 1 && $hour < 6;}, 20 );Watch out for
- Saving a template still marks its pages for generation right away; they simply wait until the window opens.
- RowSprout Pro has the same settings on RowSprout → Throttling; preferably use one or the other. If both are set, the priority
20above makes your batch size win, while the time windows add up: pages are only built when both allow it.
Import rows from a spreadsheet
Section titled “Import rows from a spreadsheet”RowSprout Pro
Situation. You have the rows in a spreadsheet: 200 towns with their name, address part, introduction and phone number. Typing them into the Groups tab is not an option.
RowSprout Pro registers its MCP tools as WordPress Abilities, which you can also call from PHP. This script reads a CSV export whose header row holds the property codes (shown on the Properties tab), adds a row per line and queues the pages:
<?php/** * Usage: wp eval-file import-rows.php <template-id> <file.csv> --user=<id> * The CSV's first line holds the property codes, e.g.: title,href,town,intro */[ $templateId, $file ] = [ (int) $args[0], $args[1] ];
// The abilities take property keys; map the codes in the header to them.$properties = wp_get_ability( 'rowsprout/list-template-properties' )->execute( [ 'template_id' => $templateId ] );if ( is_wp_error( $properties ) ) { WP_CLI::error( $properties->get_error_message() );}$keyByCode = array_column( $properties['field_types'], 'key', 'code' );
$handle = fopen( $file, 'r' );$header = fgetcsv( $handle, 0, ',', '"', '' );$unknown = array_diff( $header, array_keys( $keyByCode ) );if ( $unknown ) { WP_CLI::error( 'Unknown property codes: ' . implode( ', ', $unknown ) );}
$addGroup = wp_get_ability( 'rowsprout/add-template-group' );$rowNumber = 1; // as in the spreadsheet: row 1 is the header$added = 0;while ( ( $row = fgetcsv( $handle, 0, ',', '"', '' ) ) !== false ) { $rowNumber++; $fields = []; foreach ( $header as $i => $code ) { $fields[ $keyByCode[ $code ] ] = $row[ $i ] ?? ''; } $result = $addGroup->execute( [ 'template_id' => $templateId, 'fields' => $fields ] ); if ( is_wp_error( $result ) ) { WP_CLI::warning( "Row $rowNumber: " . $result->get_error_message() ); continue; } $added++;}fclose( $handle );
$generate = wp_get_ability( 'rowsprout/generate-pages' )->execute( [ 'template_id' => $templateId ] );if ( is_wp_error( $generate ) ) { WP_CLI::error( $generate->get_error_message() );}WP_CLI::success( "$added rows added; their pages are being generated in the background." );wp eval-file import-rows.php 123 towns.csv --user=1wp rowsprout queue-status 123 --user=1Watch out for
- Rows are only added, never updated or removed. To update existing rows from a sheet, list them first with
rowsprout/list-template-groupsand callrowsprout/update-template-groupfor the ones that exist. - Every row needs a unique value for the URL / Slug property; a duplicate is refused with a warning such as
Row 4: This group's href/slug value collides with another group on this template. - Pages are only generated for a published template. Import into a draft if you like: the rows wait, and publishing the template queues them all.
- Quoted values may contain commas and line breaks; a line break in a Textarea value stays a line break.
- The abilities check permissions like the editor does, so
--usermust be someone who may edit the template. They are only available while RowSprout Pro has a valid licence.
Regenerate everything after a deploy
Section titled “Regenerate everything after a deploy”RowSprout Pro
Situation. You changed something that every generated page depends on, such as the theme’s page template or a shared Elementor section that RowSprout copied into the pages, and want all pages rebuilt from a deploy script.
# --suppress_filters=1: with WPML, wp post list otherwise only returns the default language.for id in $(wp post list --post_type=rowsprout_template --post_status=publish --field=ID --suppress_filters=1); do wp rowsprout generate "$id" --force --user=1done
# Follow progress, or run what is due now instead of waiting for the next minute:wp rowsprout queue-status --user=1wp rowsprout process-queue --user=1Watch out for
--forcerebuilds every page, also the ones Smart Generate considers unchanged. Without it only outdated and new pages are built.- Planned rows (RowSprout Pro’s planning) are generated now as well when you use
--force. - The pages are built by the background queue, in batches, within the throttling settings. On a large site this takes a while;
queue-statusshows how far it is. process-queuehands the due pages to Action Scheduler and runs them. If Action Scheduler is already busy in the background, it reports too many concurrent batches: the pages are then being built anyway. Check withqueue-status, or runwp action-scheduler run --hooks=rowsprout_process_single_grouponce it is free.
See the WP-CLI reference for every option.