Để lấy tất cả trang con (bao gồm cả cấp 1, cấp 2, cấp 3,...) của một trang cụ thể trong WordPress
Để lấy tất cả trang con (bao gồm cả cấp 1, cấp 2, cấp 3,...) của một trang cụ thể trong WordPress bằng GraphQL, bạn cần sử dụng plugin như WPGraphQL, và truy vấn phải sử dụng đệ quy (nếu được hỗ trợ) hoặc truy cập qua các cấp con cụ thể.
Tuy nhiên, WPGraphQL không hỗ trợ đệ quy tự động, nên bạn phải lặp các cấp con thủ công nếu số cấp đã biết. Dưới đây là ví dụ minh họa:
Muốn trích xuất tất cả các trang con (bao gồm cả các cấp độ sâu hơn như "Test 2.1", "Test 2.2") từ kết quả GraphQL đã cung cấp và đưa chúng vào một mảng phẳng.
Dưới đây là cách bạn có thể thực hiện điều đó bằng JavaScript (hoặc ngôn ngữ lập trình tương tự) để xử lý dữ liệu JSON đã nhận được:
const graphQLResult = {
"data": {
"page": {
"id": "cG9zdDo1OA==",
"uri": "/en/test/",
"children": {
"edges": [
{
"node": {
"id": "cG9zdDo4NQ==",
"excerpt": "",
"title": "Test 1",
"children": {
"nodes": [
{
"id": "cG9zdDo5MA==",
"excerpt": "",
"featuredImage": null,
"title": "Test 2.2"
},
{
"id": "cG9zdDo4Nw==",
"excerpt": "",
"featuredImage": null,
"title": "Test 2.1"
}
]
}
}
}
]
}
}
}
};
function getAllChildren(pageData) {
const childrenArray = [];
function traverseChildren(nodes) {
if (!nodes) {
return;
}
nodes.forEach(node => {
// Thêm thông tin của trang con hiện tại vào mảng
childrenArray.push({
id: node.id,
excerpt: node.excerpt,
title: node.title,
featuredImage: node.featuredImage // Bao gồm featuredImage nếu có
});
// Nếu có các trang con tiếp theo, đệ quy
if (node.children && node.children.nodes) {
traverseChildren(node.children.nodes);
}
});
}
// Bắt đầu duyệt từ các con trực tiếp của trang "test"
if (graphQLResult.data.page && graphQLResult.data.page.children && graphQLResult.data.page.children.edges) {
graphQLResult.data.page.children.edges.forEach(edge => {
// Đảm bảo rằng node tồn tại trước khi xử lý
if (edge.node) {
// Thêm trang con trực tiếp (Test 1)
childrenArray.push({
id: edge.node.id,
excerpt: edge.node.excerpt,
title: edge.node.title,
featuredImage: edge.node.featuredImage
});
// Duyệt các con của Test 1 (Test 2.1, Test 2.2)
if (edge.node.children && edge.node.children.nodes) {
traverseChildren(edge.node.children.nodes);
}
}
});
}
return childrenArray;
}
const allChildPages = getAllChildren(graphQLResult);
console.log(allChildPages);
Kết quả đầu ra (từ console.log(allChildPages)
):
[
{ id: 'cG9zdDo4NQ==', excerpt: '', title: 'Test 1', featuredImage: undefined },
{ id: 'cG9zdDo5MA==', excerpt: '', title: 'Test 2.2', featuredImage: null },
{ id: 'cG9zdDo4Nw==', excerpt: '', title: 'Test 2.1', featuredImage: null }
]
Giải thích:
graphQLResult
: Đây là đối tượng JSON chứa kết quả từ truy vấn GraphQL của bạn.getAllChildren(pageData)
:Hàm này sẽ nhận đối tượng kết quả GraphQL làm đầu vào.
childrenArray
: Một mảng trống để lưu trữ tất cả các trang con đã tìm thấy.traverseChildren(nodes)
: Đây là một hàm đệ quy.Nó duyệt qua từng
node
trong danh sáchnodes
được cung cấp.Với mỗi
node
, nó lấy các thông tin cần thiết (id
,excerpt
,title
,featuredImage
) và thêm vàochildrenArray
.Nếu
node
đó có thêmchildren
(tức là nó có các trang con riêng), hàmtraverseChildren
sẽ được gọi lại vớinodes
của các trang con đó, tạo ra quá trình đệ quy để duyệt qua tất cả các cấp.
Logic chính:
Đầu tiên, nó kiểm tra xem
graphQLResult.data.page
và cácchildren.edges
có tồn tại hay không.Sau đó, nó duyệt qua các
edge
trongchildren.edges
(đây là các trang con trực tiếp của "Test", trong trường hợp này là "Test 1").Nó thêm "Test 1" vào
childrenArray
.Cuối cùng, nó gọi
traverseChildren
để xử lý các trang con của "Test 1" (là "Test 2.1" và "Test 2.2"), đảm bảo rằng tất cả các cấp độ đều được đưa vào mảng.
Cách tiếp cận này hiệu quả để làm phẳng cấu trúc cây và lấy tất cả các trang con vào một mảng duy nhất, dễ dàng để xử lý tiếp.
Khai báo với typescript
Bạn muốn khai báo hàm getAllChildren
với TypeScript, dựa trên cấu trúc dữ liệu GraphQL mà bạn đã cung cấp trước đó. Hàm này sẽ duyệt đệ quy qua các trang con để thu thập tất cả chúng vào một mảng phẳng.
Dưới đây là cách bạn có thể khai báo và triển khai hàm getAllChildren
bằng TypeScript:
Khai báo Type và Hàm getAllChildren
getAllChildren
Để khai báo hàm này một cách rõ ràng trong TypeScript, trước tiên chúng ta cần định nghĩa các interface (kiểu dữ liệu) cho cấu trúc dữ liệu mà GraphQL trả về. Điều này giúp TypeScript hiểu rõ hình dạng của dữ liệu và cung cấp tính năng tự động hoàn thành, kiểm tra lỗi tĩnh.
TypeScript
// 1. Định nghĩa các Interfaces cho dữ liệu GraphQL
interface FeaturedImageNode {
sourceUrl: string | null;
title?: string; // Tiêu đề ảnh có thể có hoặc không
}
interface FeaturedImage {
node: FeaturedImageNode | null;
}
interface Language {
code: string;
}
interface Translation {
slug: string;
language: Language;
}
// Định nghĩa interface cho một trang (page)
interface PageNode {
id: string;
excerpt: string;
title: string;
uri?: string; // URI có thể không có ở mọi cấp độ tùy thuộc truy vấn
slug?: string; // Slug có thể không có ở mọi cấp độ
featuredImage: FeaturedImage | null;
language?: Language; // Ngôn ngữ có thể có hoặc không
translations?: Translation[]; // Dịch thuật có thể có hoặc không
children?: { // Các trang con
nodes?: PageNode[]; // Đây là nơi xảy ra đệ quy
edges?: { // Nếu bạn truy vấn dùng edges
node: PageNode;
}[];
};
}
// Định nghĩa interface cho cấu trúc GraphQL đầy đủ
interface GraphQLPageResult {
data: {
page: PageNode;
};
}
// 2. Khai báo và triển khai hàm getAllChildren
/**
* Thu thập tất cả các trang con (bao gồm cả các cấp độ sâu hơn) từ một cấu trúc GraphQL phân cấp.
*
* @param graphQLResult Kết quả GraphQL chứa dữ liệu trang cha và các trang con.
* @returns Một mảng phẳng chứa tất cả các trang con được tìm thấy.
*/
function getAllChildren(graphQLResult: GraphQLPageResult): PageNode[] {
const childrenArray: PageNode[] = [];
// Hàm đệ quy để duyệt và thu thập các trang con
function traverseChildren(nodes: PageNode[] | undefined) {
if (!nodes) {
return;
}
nodes.forEach(node => {
// Thêm thông tin của trang con hiện tại vào mảng
// Clone đối tượng node để tránh tham chiếu nếu bạn cần chỉnh sửa sau này
childrenArray.push({
id: node.id,
excerpt: node.excerpt,
title: node.title,
uri: node.uri,
slug: node.slug,
featuredImage: node.featuredImage,
language: node.language,
translations: node.translations,
});
// Nếu có các trang con tiếp theo (nested children), đệ quy
if (node.children && node.children.nodes) {
traverseChildren(node.children.nodes);
} else if (node.children && node.children.edges) { // Xử lý nếu định dạng là edges
traverseChildren(node.children.edges.map(edge => edge.node));
}
});
}
// Bắt đầu duyệt từ các con trực tiếp của trang gốc
const rootPageChildren = graphQLResult.data.page?.children;
if (rootPageChildren?.nodes) {
traverseChildren(rootPageChildren.nodes);
} else if (rootPageChildren?.edges) { // Xử lý nếu định dạng là edges
traverseChildren(rootPageChildren.edges.map(edge => edge.node));
}
return childrenArray;
}
// --- Ví dụ sử dụng (dựa trên kết quả GraphQL bạn đã cung cấp) ---
const sampleGraphQLResult: GraphQLPageResult = {
"data": {
"page": {
"id": "cG9zdDo1OA==",
"uri": "/en/test/",
"excerpt": "", // Thêm các trường cần thiết nếu không có trong truy vấn gốc
"title": "Test", // Thêm tiêu đề của trang gốc
"featuredImage": null,
"children": {
"edges": [
{
"node": {
"id": "cG9zdDo4NQ==",
"excerpt": "",
"title": "Test 1",
"featuredImage": null, // Đảm bảo có featuredImage để khớp interface
"children": {
"nodes": [
{
"id": "cG9zdDo5MA==",
"excerpt": "",
"featuredImage": null,
"title": "Test 2.2"
},
{
"id": "cG9zdDo4Nw==",
"excerpt": "",
"featuredImage": null,
"title": "Test 2.1"
}
]
}
}
}
]
}
}
}
};
const allChildPages = getAllChildren(sampleGraphQLResult);
console.log(allChildPages);
Giải thích:
Định nghĩa Interfaces (
PageNode
,GraphQLPageResult
, v.v.):Đây là bước quan trọng nhất của TypeScript. Chúng ta tạo ra các "khuôn mẫu" để mô tả chính xác cấu trúc của dữ liệu mà bạn mong đợi từ GraphQL.
PageNode
là interface trung tâm, mô tả một đối tượng trang với các thuộc tính nhưid
,title
,excerpt
,featuredImage
, và quan trọng nhất làchildren
.Thuộc tính
children
trongPageNode
được định nghĩa đệ quy để có thể chứa một mảng cácPageNode
khác, phản ánh cấu trúc phân cấp. Tôi đã bao gồm cảnodes
vàedges
để phù hợp với các định dạng trả về khác nhau của WPGraphQL.GraphQLPageResult
là interface bao bọc toàn bộ kết quả GraphQL, đảm bảo cấu trúcdata.page
được xác định.
Hàm
getAllChildren(graphQLResult: GraphQLPageResult): PageNode[]
:Kiểu tham số: Hàm nhận một đối tượng
graphQLResult
có kiểuGraphQLPageResult
. TypeScript sẽ kiểm tra xem đối tượng bạn truyền vào có khớp với cấu trúc này không.Kiểu trả về: Hàm được khai báo sẽ trả về một mảng các
PageNode
(PageNode[]
).childrenArray: PageNode[] = []
: Mảng này được khai báo với kiểuPageNode[]
, đảm bảo rằng chỉ các đối tượng khớp vớiPageNode
mới có thể được thêm vào.Hàm
traverseChildren(nodes: PageNode[] | undefined)
:Đây là hàm đệ quy được sử dụng để duyệt qua các cấp độ con.
Tham số
nodes
có kiểuPageNode[] | undefined
để xử lý trường hợp không có node nào.Khi
childrenArray.push(node)
được gọi, TypeScript đảm bảo rằngnode
khớp vớiPageNode
.Logic kiểm tra
node.children && node.children.nodes
hoặcnode.children && node.children.edges
giúp hàm xử lý cả hai dạng trả về củachildren
(trực tiếpnodes
hoặc quaedges
).
Bằng cách sử dụng các interface này, bạn sẽ nhận được sự hỗ trợ mạnh mẽ từ TypeScript, giúp phát hiện lỗi cú pháp, lỗi logic sớm hơn trong quá trình phát triển, và làm cho mã nguồn của bạn dễ đọc và bảo trì hơn.
Bạn muốn chuyển đổi cấu trúc dữ liệu GraphQL phức tạp thành một mảng phẳng các đối tượng Product
đơn giản hơn, phù hợp với interface Product
mà bạn đã định nghĩa.
Product
đơn giản hơn, phù hợp với interface Product
mà bạn đã định nghĩa.Dưới đây là hàm TypeScript để thực hiện việc chuyển đổi này, bao gồm cả việc xử lý ảnh đại diện (featuredImage
).
TypeScript
// 1. Định nghĩa các Interfaces cho dữ liệu GraphQL (giữ nguyên từ lần trước, đã được điều chỉnh)
interface MediaItemNode {
__typename: "MediaItem";
sourceUrl: string | null;
title?: string;
}
interface NodeWithFeaturedImageConnectionEdge {
__typename: "NodeWithFeaturedImageToMediaItemConnectionEdge";
node: MediaItemNode;
}
interface FeaturedImageGraphQL { // Đổi tên để tránh xung đột với interface Product.img
__typename: "NodeWithFeaturedImageToMediaItemConnectionEdge"; // Thêm typename nếu có
node: MediaItemNode | null;
}
interface PageNode {
__typename: "Page";
id: string;
excerpt: string;
title: string;
uri?: string;
slug?: string;
featuredImage: FeaturedImageGraphQL | null; // Sử dụng FeaturedImageGraphQL
language?: { __typename: string; code: string; }; // Giữ nguyên nếu có
translations?: { __typename: string; slug: string; language: { __typename: string; code: string; }; }[]; // Giữ nguyên nếu có
children?: {
__typename: "HierarchicalContentNodeToContentNodeChildrenConnection";
edges?: {
__typename: "HierarchicalContentNodeToContentNodeChildrenConnectionEdge";
node: PageNode;
}[];
nodes?: PageNode[];
};
}
interface GraphQLPageResult {
data: {
page: PageNode;
};
}
// 2. Định nghĩa interface Product của bạn
interface Product {
id: string;
title: string;
img: string; // URL của ảnh featured
category: string; // Bạn cần xác định logic để gán category
}
// 3. Hàm chuyển đổi từ GraphQL sang mảng Product
/**
* Chuyển đổi dữ liệu GraphQL phân cấp thành một mảng phẳng các đối tượng Product.
*
* @param graphQLResult Kết quả GraphQL trả về.
* @returns Mảng phẳng các đối tượng Product.
*/
function convertGraphQLToProducts(graphQLResult: GraphQLPageResult): Product[] {
const products: Product[] = [];
// Hàm đệ quy để duyệt và chuyển đổi các page node thành product
function processPageNode(pageNode: PageNode) {
const imageUrl = pageNode.featuredImage?.node?.sourceUrl || ''; // Lấy sourceUrl hoặc chuỗi rỗng
// Giả định cách bạn gán category.
// Ví dụ: dựa vào một phần của title hoặc một logic phức tạp hơn.
// Hiện tại, tôi sẽ dùng một logic đơn giản hoặc placeholder.
let category: string = 'default';
if (pageNode.title.includes('Test 1')) {
category = 'category-A';
} else if (pageNode.title.includes('Test 2.1') || pageNode.title.includes('Test 2.2')) {
category = 'category-B';
}
// Bạn có thể mở rộng logic này để gán category phù hợp với yêu cầu của bạn
// Chỉ thêm vào nếu có đủ thông tin cần thiết cho một Product
products.push({
id: pageNode.id,
title: pageNode.title,
img: imageUrl,
category: category,
});
// Tiếp tục đệ quy cho các trang con
if (pageNode.children) {
if (pageNode.children.nodes) {
pageNode.children.nodes.forEach(childNode => processPageNode(childNode));
} else if (pageNode.children.edges) {
pageNode.children.edges.forEach(edge => processPageNode(edge.node));
}
}
}
// Bắt đầu xử lý từ trang gốc
// Lưu ý: Nếu bạn muốn trang gốc (ví dụ "Test") cũng là một "Product"
// thì bạn sẽ gọi processPageNode(graphQLResult.data.page);
// Nếu chỉ muốn các trang con, thì bạn sẽ làm như sau:
const rootPage = graphQLResult.data.page;
if (rootPage.children) {
if (rootPage.children.nodes) {
rootPage.children.nodes.forEach(childNode => processPageNode(childNode));
} else if (rootPage.children.edges) {
rootPage.children.edges.forEach(edge => processPageNode(edge.node));
}
}
return products;
}
// --- Dữ liệu đầu vào bạn cung cấp ---
const graphQLDataInput = {
"data": {
"page": {
"__typename": "Page",
"id": "cG9zdDo1OA==",
"uri": "/en/test/",
"children": {
"__typename": "HierarchicalContentNodeToContentNodeChildrenConnection",
"edges": [
{
"__typename": "HierarchicalContentNodeToContentNodeChildrenConnectionEdge",
"node": {
"__typename": "Page",
"id": "cG9zdDo4NQ==",
"excerpt": "",
"title": "Test 1",
"featuredImage": {
"__typename": "NodeWithFeaturedImageToMediaItemConnectionEdge",
"node": {
"__typename": "MediaItem",
"sourceUrl": "https://test1.com/wp-content/uploads/2025/05/10.webp",
"title": "10"
}
},
"children": {
"__typename": "HierarchicalContentNodeToContentNodeChildrenConnection",
"nodes": [
{
"__typename": "Page",
"id": "cG9zdDo5MA==",
"excerpt": "",
"featuredImage": null,
"title": "Test 2.2"
},
{
"__typename": "Page",
"id": "cG9zdDo4Nw==",
"excerpt": "",
"featuredImage": {
"__typename": "NodeWithFeaturedImageToMediaItemConnectionEdge",
"node": {
"__typename": "MediaItem",
"sourceUrl": "https://test1.com/wp-content/uploads/2025/05/4-scaled.jpg",
"title": "4"
}
},
"title": "Test 2.1"
}
]
}
}
}
]
}
}
}
};
// Chuyển đổi dữ liệu
const allProductsArray: Product[] = convertGraphQLToProducts(graphQLDataInput);
console.log(allProductsArray);
/*
Kết quả mong đợi:
[
{
id: "cG9zdDo4NQ==",
title: "Test 1",
img: "https://test1.com/wp-content/uploads/2025/05/10.webp",
category: "category-A"
},
{
id: "cG9zdDo5MA==",
title: "Test 2.2",
img: "", // featuredImage là null
category: "category-B"
},
{
id: "cG9zdDo4Nw==",
title: "Test 2.1",
img: "https://test1.com/wp-content/uploads/2025/05/4-scaled.jpg",
category: "category-B"
}
]
*/
Giải thích các thay đổi và logic:
Cập nhật Interfaces GraphQL:
FeaturedImageGraphQL
: Đã đổi tên để tránh nhầm lẫn với trườngimg
trongProduct
.Đảm bảo
__typename
được bao gồm ở tất cả các cấp nơi nó xuất hiện trong dữ liệu của bạn.
Hàm
convertGraphQLToProducts(graphQLResult: GraphQLPageResult): Product[]
:products: Product[] = []
: Khởi tạo một mảng rỗng để lưu trữ các đối tượngProduct
đã chuyển đổi.processPageNode(pageNode: PageNode)
: Đây là hàm đệ quy chính:Nó nhận một
PageNode
làm đối số.Lấy
img
(sourceUrl):TypeScriptconst imageUrl = pageNode.featuredImage?.node?.sourceUrl || '';
Dòng này sử dụng optional chaining (
?.
) để truy cập an toàn vàofeaturedImage
,node
vàsourceUrl
. Nếu bất kỳ phần nào trên đường dẫn lànull
hoặcundefined
, nó sẽ trả vềundefined
, và sau đó toán tử|| ''
sẽ gán một chuỗi rỗng.Gán
category
:Tôi đã thêm một logic giả định để gán
category
dựa trêntitle
của trang (if (pageNode.title.includes('Test 1')) { category = 'category-A'; }
).Bạn cần thay đổi logic này để phù hợp với cách bạn muốn phân loại các sản phẩm của mình. Ví dụ, bạn có thể có một trường custom field trong WordPress lưu trữ category của sản phẩm, và bạn sẽ cần truy vấn trường đó qua GraphQL để lấy thông tin category.
Thêm vào
products
array: Một đối tượngProduct
mới được tạo vớiid
,title
(từpageNode.title
),img
(từimageUrl
), vàcategory
và đượcpush
vào mảngproducts
.Đệ quy cho
children
: Hàm này sau đó kiểm trapageNode.children
và gọi đệ quyprocessPageNode
cho tất cả các trang con (dù chúng nằm trongnodes
hayedges
).
Logic bắt đầu:
Hàm bắt đầu bằng cách lấy
rootPage
(graphQLResult.data.page
).Nó sau đó kiểm tra
rootPage.children
và bắt đầu quá trình đệ quyprocessPageNode
cho tất cả các trang con trực tiếp của trang gốc.Nếu bạn muốn bản thân trang gốc (ví dụ "Test") cũng được chuyển đổi thành một "Product" và thêm vào danh sách, bạn chỉ cần gọi
processPageNode(rootPage);
trước khi xử lý các con của nó. Hiện tại, nó chỉ xử lý các trang con của "Test".
Với hàm này, bạn có thể biến dữ liệu GraphQL phân cấp của mình thành một mảng phẳng các đối tượng Product
dễ dàng quản lý trong ứng dụng Next.js của bạn.
Generate Audio Overview
src\graphql\queries.ts
import { gql } from '@apollo/client'
export const GET_HOMEPAGE = gql`
query {
page(id: "home", idType: URI) {
title
content
translations {
uri
language {
code
}
}
}
}
`
export const GET_POST = gql`
query GetPost($slug: String!) {
postBy(slug: $slug) {
databaseId
title
content
slug
featuredImage {
node {
sourceUrl
altText
title
}
}
categories {
nodes {
id
name
slug
}
}
tags {
nodes {
id
name
slug
}
}
translations {
uri
language {
code
}
}
language {
code
}
}
}
`
export const GET_RELATED_POSTS = gql`
query GetRelatedPosts($slug: ID!) {
post(id: $slug,idType: SLUG) {
categories {
nodes {
id
posts(first: 4) {
nodes {
title
slug
uri
featuredImage {
node {
sourceUrl
altText
title
}
}
}
}
}
}
tags {
nodes {
id
posts(first: 4) {
nodes {
title
slug
uri
}
}
}
}
}
}`;
// GET_CHILD_POSTS
export const GET_CHILD_POSTS = gql`
query GetChildPosts($slug: String!) {
page(id: $slug, idType: URI) {
id
uri
children {
edges {
node {
id
... on Page {
id
excerpt
title
featuredImage {
node {
sourceUrl
title
}
}
children {
nodes {
... on Page {
id
excerpt
featuredImage {
node {
sourceUrl
title
}
}
title
}
}
}
}
}
}
}
}
}
`;
export const GET_PAGE = gql`
query GetPage($slug: String!) {
pageBy(uri: $slug) {
title
content
featuredImage {
node {
sourceUrl
}
}
translations {
uri
language {
code
}
}
language {
code
}
}
}
`;
// Query để lấy danh sách chuyên mục
export const GET_CATEGORIES = gql`
query GetCategories {
categories {
nodes {
slug
name
language {
code
}
translations {
slug
language {
code
}
}
}
}
}
`;
// Query để lấy bài viết theo chuyên mục
export const GET_POSTS_BY_CATEGORY = gql`
query GetPostsByCategory($slug: ID!) {
category(id:$slug, idType: SLUG) {
slug
name
language {
code
}
translations {
slug
language {
code
}
}
posts {
nodes {
slug
title
content
language {
code
}
translations {
slug
language {
code
}
}
}
}
}
}
`;
// Query để lấy bài viết mới nhất
export const GET_POSTS_NEW = gql`
query GetPosts($language: LanguageCodeFilterEnum!) {
posts(before: "10", where: {language: $language}) {
nodes {
title
excerpt
date
uri
slug
featuredImage {
node {
sourceUrl
}
}
language {
code
}
translations {
slug
language {
code
}
}
}
}
}
`;
export const GET_SINGLE_SEO_METADATA = gql`
query GetPageSEOMetadata($uri: ID!) {
page(id: $uri, idType: URI) {
title
seo {
title
description
canonicalUrl
breadcrumbTitle
robots
focusKeywords
fullHead
jsonLd {
raw
}
breadcrumbs {
text
url
}
}
}
}
`;
// Query để lấy bài tất cả bài viêt
export const GET_ALL_POSTS = gql`
query GetPosts {
posts {
nodes {
title
excerpt
date
uri
slug
featuredImage {
node {
sourceUrl
}
}
language {
code
}
translations {
slug
language {
code
}
}
}
}
}
`;
Last updated
Was this helpful?